방명록 만들기

✒️ 2025-05-28 11:47 내용 수정


실습 목표


실습 흐름

  1. DB와 Spring을 Mybatis로 연결한다.
  2. DB에 테이블을 추가한다.
  3. DTO와 DAO를 만들고, Context_3_dao에 DAO Bean을 추가한다.
  4. 조회, 수정, 삭제 기능을 DAO에 추가한다.
  5. DAO에 추가한 내용의 query문을 mapper에도 추가한다.
  6. JSP 페이지를 만들어 각 기능에 맞는 데이터를 보내거나 받을 수 있도록 작성한다.
  7. Controller를 생성하여 DAO를 호출하고, JSP에서 보낸 데이터를 이어준다.
  8. ServletContext에 Controller Bean을 추가한다.

흐름 내용

DB에 테이블 추가

-- 시퀀스
CREATE SEQUENCE SEQ_VISIT_IDX;

-- 테이블
CREATE TABLE VISIT(
	IDX NUMBER(3) PRIMARY KEY,
	NAME VARCHAR2(50),
	CONTENT VARCHAR2(1000),
	PWD VARCHAR2(50),
	IP VARCHAR2(20),
	REGDATE DATE
);

-- 샘플데이터
INSERT INTO VISIT VALUES(
	SEQ_VISIT_IDX.nextVal,
	'홍길동',
	'첫 게시글을 작성함',
	'1111',
	'192.1.1.1',
	SYSDATE
);

-- 데이터 커밋
COMMIT;

Spring 프로젝트 환경설정 및 전체 파일 위치

visit 1.png

DB 연결

  1. Context_1_dataSource
package context;

import javax.sql.DataSource;

import org.apache.commons.dbcp.BasicDataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class Context_1_dataSource {
	
	@Bean
	public DataSource ds() {
		BasicDataSource ds = new BasicDataSource();
		ds.setDriverClassName("oracle.jdbc.OracleDriver");
		ds.setUrl("jdbc:oracle:thin:@localhost:1521:xe");
		ds.setUsername("계정명");
		ds.setPassword("비밀번호");
		return ds;
	}
}
  1. Context_2_myBatis
package context;

import javax.sql.DataSource;

import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.SqlSessionTemplate;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;

import lombok.RequiredArgsConstructor;

@Configuration
@RequiredArgsConstructor
public class Context_2_myBatis {
	
	final DataSource ds;
	
	@Bean
	public SqlSessionFactory factoryBean() throws Exception{
		SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
		
		factoryBean.setDataSource(ds);
		
		// mapper를 알고있는 mybatis-config.xml 파일의 위치를 알려줘야 함
		factoryBean.setConfigLocation(new ClassPathResource("config/mybatis/mybatis-config.xml"));
		
		return factoryBean.getObject();
	}

	@Bean
	public SqlSessionTemplate sqlSessionBean() throws Exception {
		return new SqlSessionTemplate(factoryBean());
	}
}
  1. mybatis-config.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "HTTP://mybatis.org/dtd/mybatis-3-config.dtd">

<configuration>
	<settings>
		<setting name="cacheEnabled" value="false" />
		<setting name="useGeneratedKeys" value="true" />
		<setting name="defaultExecutorType" value="REUSE" />
	</settings>
	
	<typeAliases>
		<typeAlias type="dto.VisitDTO" alias="visit"/>
	</typeAliases>
	
	<mappers>
		<mapper resource="config/mybatis/mapper/visit.xml" />
	</mappers>
</configuration>
  1. mapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="v">
	<!-- 방명록 조회하기 -->
	<select id="visit_list" resultType="visit">
		SELECT * FROM VISIT ORDER BY IDX DESC
	</select>
	
	<!-- 특정 방명록 조회하기 -->
	<select id="visit_one" parameterType="int" resultType="visit">
		SELECT * FROM VISIT WHERE IDX=#{idx}
	</select>
	
	<!-- 새 방명록 추가하기 -->
	<insert id="visit_insert" parameterType="visit">
		INSERT INTO VISIT VALUES(
			seq_visit_idx.nextVal,
			#{name},
			#{content},
			#{pwd},
			#{ip},
			sysdate
		)
	</insert>
	
	<!-- 방명록 삭제하기 -->
	<delete id="visit_delete" parameterType="java.util.HashMap">
		DELETE FROM VISIT WHERE IDX=#{idx} AND PWD=#{pwd}
	</delete>
	
	<!-- 방명록 수정하기 -->
	<update id="visit_modify" parameterType="visit">
		UPDATE VISIT 
		SET CONTENT = #{content},
			PWD = #{pwd},
			IP = #{ip},
			REGDATE = sysdate
		WHERE IDX=#{idx}
	</update>
</mapper>

Ajax

var xhr = null;

function createRequest() {
	if (xhr != null) {
		return;
	}
	if (window.ActiveXObject) {
		xhr = new ActiveXObject("Microsoft.XMLHTTP"); // IE 환경
	} else {
		xhr = new XMLHttpRequest(); // 기타 브라우저 환경
	}
}

function sendRequest(url, param, callback, method) {

	// HttpRequest 생성
	createRequest();

	// 전송 타입 구분
	var httpMethod = (method != 'POST' && method != 'post') ? 'GET' : 'POST';

	// 파라미터 구분
	var httpParam = (param == null || param == '') ? null : param;

	// 접근 url
	var httpURL = url;

	// 요청 방식이 GET이고 전달할 파라미터가 있다면 새 url 경로 제작
	if (httpMethod == 'GET' && httpParam != null) {
		httpURL = httpURL+'?'+httParam;
	}

	// 서버로 보낼 Ajax 요청 형식
	xhr.open(httpMethod, httpURL, true);

	// requestHeader 설정 : Content-Type 지정
	xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");

	// 작업이 완료된 후 호출할 callback 메소드 지정
	xhr.onreadystatechange = callback;

	// Ajax 요청을 서버로 전달
	xhr.send(httpMethod == 'POST' ? httpParam : null);
}

DTO와 DAO

  1. dto
    • lombok 라이브러리의 getter와 setter 사용
package dto;

import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
public class VisitDTO {
	private int idx;
	private String name, content, pwd, ip, regdate;
}
  1. dao
package dao;

import java.util.HashMap;
import java.util.List;

import org.apache.ibatis.session.SqlSession;

import dto.VisitDTO;

public class VisitDAO {

	SqlSession sqlSession;
	
	public VisitDAO(SqlSession sqlSession) {
		this.sqlSession = sqlSession;
	}
	
	// 방명록 전체 조회하기
	public List<VisitDTO> selectList() {
		List<VisitDTO> list = sqlSession.selectList("v.visit_list");
		return list;
	}
	
	// 특정 방명록 조회하기
	public VisitDTO selectOne(int idx) {
		VisitDTO dto = sqlSession.selectOne("v.visit_one", idx);
		return dto;
	}
	
	// 새 방명록 추가하기
	public int insert(VisitDTO dto) {
		int res = sqlSession.insert("v.visit_insert", dto);
		return res;
	}
	
	// 방명록 삭제하기
	public int delete(HashMap<String, Object> map) {
		int res = sqlSession.delete("v.visit_delete", map);
		return res;
	}
	
	// 방명록 수정하기
	public int modify(VisitDTO dto) {
		int res = sqlSession.update("v.visit_modify", dto);
		return res;
	}
}
  1. Context_3_dao
    • DAO Bean 객체를 등록
package context;

import org.apache.ibatis.session.SqlSession;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import dao.VisitDAO;

@Configuration
public class Context_3_dao {
	
	@Bean
	public VisitDAO visit_daoBean(SqlSession sqlSession) {
		return new VisitDAO(sqlSession);
	}
}

Controller

  1. VisitController
    • JSP 실습 때 Servlet이 하던 역할을 담당함
package com.nogroup.visit;

import java.util.HashMap;
import java.util.List;

import javax.servlet.http.HttpServletRequest;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import dao.VisitDAO;
import dto.VisitDTO;
import util.MyCommon;

@Controller
public class VisitController {

	VisitDAO visit_dao;
	
	public VisitController(VisitDAO visit_dao) {
		this.visit_dao = visit_dao;
	}
	
	// 게시글 조회 페이지로 mapping
	@RequestMapping(value= {"/", "visit_list"})
	public String select(Model model) {
		List<VisitDTO> list = visit_dao.selectList();
		model.addAttribute("list", list);
		return MyCommon.VIEW_PATH+"visit_list.jsp";
	}
	
	// 게시글 추가 페이지로 mapping
	@RequestMapping(value= "insert_form")
	public String insert_form() {
		return MyCommon.VIEW_PATH+"insert_form.jsp";
	}
	
	// 게시글 추가
	@RequestMapping(value= "insert")
	public String insert(VisitDTO dto, HttpServletRequest request) {
		// insert?name=이름&content=내용&pwd=비밀번호
		String ip = request.getRemoteAddr();
		dto.setIp(ip);
		
		int res = visit_dao.insert(dto);
		
		// sendRedirect("visit_list")
		return "redirect:visit_list";
	}
	
	// 게시글 삭제 mapping
	@RequestMapping(value="delete")
	@ResponseBody
	public String delete(int idx, String pwd) {
		HashMap<String, Object> map = new HashMap<String, Object>();
		map.put("idx", idx);
		map.put("pwd", pwd);
		
		int res = visit_dao.delete(map);
		
		String result = "no";
		
		if (res == 1) {
			result = "yes";
		}
		
		String finRes = String.format("[{'res':'%s'}]", result);
		
		// return 값을 callback 함수로 돌아감을 표시하는 @ResponseBody
		return finRes; 
	}
	
	// 게시글 수정 페이지로 이동
	@RequestMapping(value="modify_form")
	public String modify_form(Model model, int idx) {
		VisitDTO dto = visit_dao.selectOne(idx);
		model.addAttribute("dto", dto);
		return MyCommon.VIEW_PATH+"visit_modify_form.jsp";
	}
	
	// 게시글 수정하기
	@RequestMapping(value="modify")
	public String modify(VisitDTO dto, HttpServletRequest request) {
		String ip = request.getRemoteAddr();
		dto.setIp(ip);
		
		int res = visit_dao.modify(dto);
		
		return "redirect:visit_list";
	}
}
  1. ServletContext
    • VisitController Bean 객체를 등록
package mvc;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import com.nogroup.visit.VisitController;

import dao.VisitDAO;

@Configuration
@EnableWebMvc
//@ComponentScan("com.nogroup.visit")
public class ServletContext implements WebMvcConfigurer{
	@Override
	public void addResourceHandlers(ResourceHandlerRegistry registry) {
		registry.addResourceHandler("/resources/**").addResourceLocations("/resources/");
	}
	
	@Bean
	public VisitController visitController(VisitDAO visit_dao) {
		return new VisitController(visit_dao);
	}

}

util 패키지

  1. MyCommon 클래스
    • JSP 경로를 저장하기 위해 사용
package util;

public class MyCommon {
	
	public static String VIEW_PATH = "/WEB-INF/views/visit/";
}

JSP

  1. 방명록 조회 jsp
    • Spring에서는 jsp에서 jsp로 이동하지 못한다.
    • callback 함수에서 eval() 대신 (new Function('return'+data))()을 사용한 이유
      • eval()은 문자로 표현된 Javascript 코드를 실행하는 함수이므로, 악영향을 줄 수 있는 문자열을 실행 시 프로그램에 문제가 발생할 수 있다.
      • 또한 eval()이 호출된 위치의 스코프를 제 3자가 볼 수 있으며, Function()으로 실행할 수 없는 공격을 eval()로는 할 수 있기에 보안상의 이유로 function()을 사용하는 것이 좋다.
<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
	<link rel="stylesheet" href="${pageContext.request.contextPath}/resources/css/visit.css">
	<script src="${pageContext.request.contextPath}/resources/js/HttpRequest.js"></script>
	<script>
		function del(f) {
			let idx = f.idx.value;
			let pwd = f.pwd.value.trim();
			let ori_pwd = f.ori_pwd.value.trim();
			
			if(pwd == '') {
				alert('비밀번호를 입력하세요');
				return;
			}
			
			if(pwd != ori_pwd) {
				alert('비밀번호가 일치하지 않습니다');
				return;
			}
			
			if(!confirm('삭제하시겠습니까?')) {
				return;
			}
			
			let url = "delete";
			let param = "idx="+idx+"&pwd="+encodeURIComponent(pwd);
			
			sendRequest(url, param, resultFn, "POST");
		}
		
		function resultFn() {
			if(xhr.readyState == 4 && xhr.status == 200) {
				let data = xhr.responseText;
				let json = (new Function('return'+data))();
				
				if(json[0].res == "no") {
					alert('삭제를 실패했습니다');
					return;
				}
				
				alert('성공적으로 삭제했습니다');
				location.href="visit_list";
			}
		}
		
		function modify(f) {
			let ori_pwd = f.ori_pwd.value.trim();
			let pwd = f.pwd.value.trim();
			
			if(pwd != ori_pwd) {
				alert('비밀번호가 일치하지 않습니다');
				return;
			}
			
			f.action = "modify_form";
			f.method = "POST";
			f.submit();
		}
	</script>
</head>
<body>
	<div id="main_box">
		<h1>방명록 리스트</h1>
		<input type="button" value="글쓰기" onclick="location.href='insert_form'">
	</div>
	
	<c:forEach var="dto" items="${list}">
		<div class="visit_box">
			<div class="type_content"><pre>${dto.content}</pre></div>
			<div class="type_name">작성자 : ${dto.name}(${dto.ip})</div>
			<div class="type_regdate">작성일 : ${dto.regdate}</div>
			<div>
				<form>
					<input type="hidden" name="idx" value="${dto.idx}">
					<input type="hidden" name="ori_pwd" value="${dto.pwd}">
					비밀번호 <input type="password" name="pwd">
					<input type="button" value="수정" onclick="modify(this.form)">
					<input type="button" value="삭제" onclick="del(this.form)">
				</form>
			</div>
		</div>
	</c:forEach>
</body>
</html>
  1. 방명록 추가 페이지
<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
	<script>
		function send(f) {
			let name = f.name.value;
			let content = f.content.value;
			let pwd = f.pwd.value;
			
			if (name == '') {
				alert('이름을 입력하세요');
				return;
			}
			
			if (content == '') {
				alert('내용을 한 글자 이상 입력하세요');
				return;
			}
			
			if (pwd == '') {
				alert('비밀번호를 입력하세요');
				return;
			}
			
			f.action = "insert";
			f.submit();
		}
	</script>
</head>
<body>
	<form>
		<table border="1" align="center">
			<caption>::새 글 작성하기::</caption>
			<tr>
				<th>작성자</th>
				<td><input name="name" style="width:250px;"></td>
			</tr>
			<tr>
				<th>내용</th>
				<td>
					<textarea row="5" cols="50" name="content" style="resize:none; wrap:on"></textarea>
				</td>
			</tr>
			<tr>
				<th>비밀번호</th>
				<td><input name="pwd" type="password"></td>
			</tr>
			<tr>
				<td colspan="2" align="center">
					<input type="button" value="등록하기" onclick="send(this.form)">
					<input type="button" value="목록으로" onclick="location.href='visit_list'">
				</td>
			</tr>
		</table>
	</form>
</body>
</html>
  1. 방명록 수정 페이지
<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
	<script type="text/javascript">
		function send(f) {
			let content = f.content.value;
			
			if (content == '') {
				alert('내용을 한 글자 이상 입력하세요');
				return;
			}
			
			f.action = "modify";
			f.method = "POST";
			f.submit();
		}
	</script>
</head>
<body>
	<form>
		<input type="hidden" name="idx" value="${dto.idx}">
		
		<table border="1" align="center">
			<caption>::방명록 수정하기::</caption>
			<tr>
				<th>작성자</th>
				<td>${dto.name}</td>
			</tr>
			<tr>
				<th>내용</th>
				<td>
					<textarea name="content" row="5" cols="50" style="resize:none;wrap=on">${dto.content}</textarea>
				</td>
			</tr>
			<tr>
				<th>비밀번호</th>
				<td><input type="password" name="pwd" value="${dto.pwd}"></td>
			</tr>
			<tr>
				<td colspan="2" align="center">
					<input type="button" value="수정" onclick="send(this.form)">
					<input type="button" value="취소" onclick="location.href='visit_list'">
				</td>
			</tr>
		</table>
	</form>
</body>
</html>

css

@charset "UTF-8";
*{margin:0; padding:0;}

#main_box{
	width:330px;
	margin:auto;
}

h1{
	text-align:center;
	margin-top:10px 0;
	color:#0080ff;
	text-shadow:2px 2px 2px black;
}

.visit_box{
	margin:30px auto 0;
	width:330px;
	box-shadow:2px 2px 2px black;
	border:1px solid blue;
}

.type_content{
	min-heigth:100px;
	height:auto;
	background-color:#fcc;
}

.type_name{
	background-color:#cfc;
}

.type_regdate{
	background-color:#ccf;
}

완성된 모습

  1. 방명록 전체가 조회된다.
    visit 2.png

  2. 글쓰기 버튼을 누르면 새 글 작성 화면이 나오며, 새 글에 필요한 정보를 입력할 수 있다.
    visit 3.png

  3. 등록하기 버튼을 누르면 추가된 방명록을 조회 화면에서 볼 수 있다.
    visit 4.png

  4. 수정하려는 방명록에 맞는 비밀번호를 입력하면 방명록을 수정할 수 있다.
    visit 5.png

  5. 수정 버튼을 누르면 바뀐 내용이 반영된 모습을 확인할 수 있다.
    visit 6.png

  6. 방명록에 맞는 비밀번호를 입력하고 삭제를 누르면 방명록이 성공적으로 삭제되는 것을 볼 수 있다.
    visit 7.png
    visit 8.png
    visit 9.png